﻿
namespace Patterns4Net.PatternEnforcer.Core.MethodCheckers
{
    using System.Diagnostics.Contracts;
    using Patterns4Net.Core.CecilExtensions;
    using System.Linq;
    using Mono.Cecil;
    using Mono.Cecil.Cil;
    using System.Collections.Generic;

    /// <summary>
    /// Checks whether given method does not contain instructions that
    /// changes value of any of object fields.
    /// </summary>
    public class DoesNotChangeInternalState : IChecker<MethodDefinition>
    {
        private readonly IEnumerable<ParameterDefinition> parameters;

        private readonly IEnumerable<FieldReference> fields;

        /// <summary>
        /// Initializes a new instance of the <see cref="DoesNotChangeInternalState"/> class.
        /// </summary>
        /// <param name="parameters">The parameters which should be also checked.</param>
        public DoesNotChangeInternalState(
            IEnumerable<ParameterDefinition> parameters = null, 
            IEnumerable<FieldReference> fields = null)
        {
            this.parameters = parameters ?? Enumerable.Empty<ParameterDefinition>();
            this.fields = fields ?? Enumerable.Empty<FieldReference>();
        }

        public CheckerResult Check(MethodDefinition methodDefinition)
        {
            if (methodDefinition.HasBody == false)
            {
                return CheckerResult.CreateValidResult();
            }

            var result = new CheckerResultBuilder();
            var instructions = methodDefinition.Body.Instructions;
            var stackStates = StackObserver.CreateStackStates(methodDefinition);

            for (int i = 0; i < instructions.Count; i++)
            {
                var instruction = instructions[i];

                // 0x7D stfld <field> Replace the value of field of the object obj with value.
                if (instruction.OpCode == OpCodes.Stfld)
                {
                    Contract.Assert(0 < i && i < stackStates.Length);

                    // Now the stack should look like this:
                    // on the top there is a value that is going to be assigned to given field
                    // just under the top there is an instance whose field is going to be assigned to.
                    var targetObject = stackStates[i - 1].Reverse().Skip(1).First();

                    // Is it at least of the same type?
                    if (targetObject.ItemType.IsEqual(methodDefinition.DeclaringType))
                    {
                        result.AddCheckedRule();

                        // targetObject whose field was assigned may be a method argument, another field
                        // or local variable. If it is local variable we have to check whether it's 
                        // value didn't come from an argument or another field (it may be more than one possible source). 
                        var possibleTargetObjects = StackObserver.GetPossibleRoots(methodDefinition, stackStates, targetObject);

                        // Now check whether it violates some rule
                        if (possibleTargetObjects.Any(x => x.IsThisParameter))
                        {
                            result.Add(ErrorMessages.ChangesInternalStateOfThis, methodDefinition.Name, instruction.Operand.ToString());
                        }
                        // chnages any of forbidden parameters
                        else if (possibleTargetObjects.Any(x => x.IsParameter && this.ShouldBeChecked(x.ParameterIndex)))
                        {
                            result.Add(
                                ErrorMessages.ChangesInternalStateOfParam,
                                methodDefinition.Name,
                                instruction.Operand.ToString());
                        }

                        // changes any of forbidden fields
                        var fieldChangingStackItem =
                            possibleTargetObjects.FirstOrDefault(x => x.IsField && this.ShoudBeChecked(x.FieldReference));
                        if (fieldChangingStackItem != null)
                        {
                            result.Add(
                                ErrorMessages.ChangesInternalStateOfField,
                                methodDefinition.Name,
                                fieldChangingStackItem.FieldReference.Name);
                        }
                    }
                }
            }

            return result.Build();
        }

        private bool ShoudBeChecked(FieldReference field)
        {
            return this.fields.Any(f => string.CompareOrdinal(field.FullName, f.FullName) == 0);
        }

        private bool ShouldBeChecked(int paremterIndex)
        {
            return this.parameters.Any(p => p.Index == paremterIndex);
        }
    }
}
